package com.leonzhangxf.ioc;

import com.google.common.collect.Sets;
import com.leonzhangxf.ioc.annotation.Autowired;
import com.leonzhangxf.ioc.annotation.Component;
import com.leonzhangxf.ioc.util.BeanNameUtils;
import com.leonzhangxf.ioc.util.ClassUtils;
import com.leonzhangxf.ioc.util.CollectionUtils;
import com.leonzhangxf.ioc.util.StringUtils;

import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

/**
 * IOC framework facade.
 *
 * @author leonzhangxf
 */
public class Injector {

    private final Class appClass;

    private Injector(Class appClass) throws Exception {
        this.appClass = appClass;
        // initialize bean factory.
        this.beanFactory = new BeanFactoryImpl();
        initialize();
    }

    private void initialize() throws Exception {
        // get all class under the package of given appClass
        Set<Class<?>> classes = ClassUtils.getClassesFromPackage(this.appClass.getPackageName());
        if (CollectionUtils.isEmpty(classes)) {
            return;
        }

        // component bean resolve.
        Set<Class<?>> componentClassSet = classes.stream()
            .filter(clazz -> {
                // filter class with Component annotation
                Component component = clazz.getAnnotation(Component.class);
                return !clazz.isInterface() && null != component;
            }).collect(Collectors.toSet());

        // bean definition resolve
        Set<DefaultBeanDefinition> beanDefinitions = componentClassSet.stream().map(clazz -> {
            DefaultBeanDefinition bd = new DefaultBeanDefinition();
            Component component = clazz.getAnnotation(Component.class);
            String id = component.value();
            if (StringUtils.hasText(id)) {
                bd.setId(id);
            } else {
                bd.setId(BeanNameUtils.generateBeanId(clazz));
            }
            bd.setClazz(clazz);

            Set<Class<?>> interfaces = new HashSet<>(Arrays.asList(clazz.getInterfaces()));
            bd.setInterfaces(interfaces);

            Method[] methods = clazz.getMethods();
            Set<Dependency> deps = Arrays.stream(methods).filter(method -> {
                Autowired autowired = method.getAnnotation(Autowired.class);
                int parameterCount = method.getParameterCount();
                return null != autowired && parameterCount == 1;
            }).map(method -> {
                Class<?> depsClass = method.getParameters()[0].getType();
                Dependency dependency = new Dependency();
                dependency.setMethod(method);
                dependency.setDepsClass(depsClass);
                return dependency;
            }).collect(Collectors.toSet());
            bd.setDependencies(deps);
            return bd;
        }).collect(Collectors.toSet());

        this.beanFactory.registerBeanDefinitions(beanDefinitions);
        this.beanFactory.initializeBeans();
    }

    private final BeanFactory beanFactory;

    public BeanFactory provideBeanFactory() {
        if (null == this.beanFactory) {
            throw new RuntimeException("BeanFactory not be created.");
        }
        return this.beanFactory;
    }

    public static <T> InjectorBuilder<T> builder() {
        return new InjectorBuilder<>();
    }

    public static class InjectorBuilder<T> {

        private Class appClass;

        public Injector build(Class appClass) throws Exception {
            return new Injector(appClass);
        }
    }
}
